<FrameworkSwitchCourse {fw} />

# Clasificarea tokenilor[[token-classification]]

{#if fw === 'pt'}

<CourseFloatingBanner chapter={7}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/en/chapter7/section2_pt.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/en/chapter7/section2_pt.ipynb"},
]} />

{:else}

<CourseFloatingBanner chapter={7}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/en/chapter7/section2_tf.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/en/chapter7/section2_tf.ipynb"},
]} />

{/if}

Prima aplicație pe care o vom explora este clasificarea tokenilor. Această sarcină generică cuprinde orice problemă care poate fi formulată ca "atribuirea unui label fiecărui token dintr-o propoziție", cum ar fi:

- **Named entity recognition(NER)**: Găsirea entităților (cum ar fi persoane, locații sau organizații) într-o propoziție. Acest lucru poate fi formulat ca atribuirea unui label fiecărui token, având o clasă pentru fiecare entitate și o clasă pentru "nicio entitate".
- **Part-of-speech tagging (POS)**: Marchează fiecare cuvânt dintr-o propoziție ca corespunzând unei anumite părți de vorbire (cum ar fi substantiv, verb, adjectiv etc.).
- **Chunking**: Găsirea tokenilor care aparțin aceleiași entități. Această sarcină (care poate fi combinată cu POS sau NER) poate fi formulată ca atribuirea unui label (de obicei `B-`) tuturor tokenilor care se află la începutul unui chunk, a unui alt label (de obicei `I-`) la tokeni care se află în interiorul unui chunk și a unui al treilea label (de obicei `O`) la tokeni care nu aparțin niciunui chunk.

<Youtube id="wVHdVlPScxA"/>

Desigur, există multe alte tipuri de probleme de clasificare a tokenilor; acestea sunt doar câteva exemple reprezentative. În această secțiune, vom pune la punct un model (BERT) pe o sarcină NER, care va fi apoi capabil să calculeze predicții precum aceasta:

<iframe src="https://course-demos-bert-finetuned-ner.hf.space" frameBorder="0" height="350" title="Gradio app" class="block dark:hidden container p-0 flex-grow space-iframe" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"></iframe>

<a class="flex justify-center" href="/huggingface-course/bert-finetuned-ner">
<img class="block dark:hidden lg:w-3/5" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter7/model-eval-bert-finetuned-ner.png" alt="One-hot encoding labels pentru răspunderea la întrebări."/>
<img class="hidden dark:block lg:w-3/5" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter7/model-eval-bert-finetuned-ner-dark.png" alt="One-hot encoded labels pentru răspunderea la întrebări."/>
</a>

Puteți găsi modelul pe care îl vom antrena și încărca în Hub și puteți verifica de două ori predicțiile sale [aici](https://huggingface.co/huggingface-course/bert-finetuned-ner?text=My+name+is+Sylvain+and+I+work+at+Hugging+Face+in+Brooklyn).

## Pregătirea datelor[[preparing-the-data]]

În primul rând, avem nevoie de un dataset adecvat pentru clasificarea simbolurilor. În această secțiune vom utiliza [datasetul CoNLL-2003] (https://huggingface.co/datasets/conll2003), care conține știri de la Reuters.

> [!TIP]
> 💡 Atât timp cât datasetul vostru constă în texte împărțite în cuvinte cu labelurile corespunzătoare, veți putea adapta procedurile de preprocesare a datelor descrise aici la propriul dataset. Consultați [Capitolul 5](/course/chapter5) dacă aveți nevoie de o recapitulare a modului de încărcare a propriilor date personalizate într-un `Dataset`.

### Datasetul CoNLL-2003 [[the-conll-2003-dataset]]

Pentru a încărca datasetul CoNLL-2003, folosim metoda `load_dataset()` din biblioteca 🤗 Datasets:

```py
from datasets import load_dataset

raw_datasets = load_dataset("conll2003")
```

Acest lucru va descărca și va stoca în cache datasetul, așa cum am văzut în [Capitolul 3](/course/chapter3) pentru setul de date GLUE MRPC. Inspectarea acestui obiect ne arată coloanele prezente și împărțirea între seturile de antrenare, validare și testare:

```py
raw_datasets
```

```python out
DatasetDict({
    train: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 14041
    })
    validation: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3250
    })
    test: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3453
    })
})
```

În special, putem vedea că datasetul conține labeluri pentru cele trei sarcini pe care le-am menționat mai devreme: NER, POS și Chunking. O mare diferență față de alte datseturi este că textele de intrare nu sunt prezentate ca propoziții sau documente, ci ca liste de cuvinte (ultima coloană se numește `tokens`, dar conține cuvinte în sensul că acestea sunt inputuri pre-tokenizate care mai trebuie să treacă prin tokenizer pentru tokenizarea subcuvintelor).

Să aruncăm o privire la primul element al setului de formare:

```py
raw_datasets["train"][0]["tokens"]
```

```python out
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
```

Deoarece dorim să efectuăm named entity recognition, ne vom uita la etichetele NER:

```py
raw_datasets["train"][0]["ner_tags"]
```

```python out
[3, 0, 7, 0, 0, 0, 7, 0, 0]
```

Acestea sunt labelurile ca numere întregi pregătite pentru antrenare, dar nu sunt neapărat utile atunci când dorim să inspectăm datele. La fel ca în cazul clasificării textului, putem accesa corespondența dintre aceste numere întregi și numele labelurilor consultând atributul `features` al datasetului nostru:

```py
ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
```

```python out
Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)
```

Deci, această coloană conține elemente care sunt secvențe a `ClassLabel`. Tipul elementelor din secvență se află în atributul `feature` al acestei `ner_feature`, iar noi putem accesa lista de nume consultând atributul `names` al acelei `feature`:

```py
label_names = ner_feature.feature.names
label_names
```

```python out
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
```

Am văzut deja aceste labeluri atunci când am cercetat pipelineul `token-classification` în [Capitolul 6](/course/chapter6/3), dar pentru o reîmprospătare rapidă:

- `O` înseamnă că cuvântul nu corespunde niciunei entități.
- `B-PER`/`I-PER` înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip *persoană*.
- `B-ORG`/`I-ORG` înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip *organizaționale*.
- `B-LOC`/`I-LOC` înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip  *locație*.
- `B-MISC`/`I-MISC` înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip *miscellaneous.

Acum, decodificarea labelurilor pe care le-am văzut mai devreme ne dă următorul rezultat:

```python
words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
    full_label = label_names[label]
    max_length = max(len(word), len(full_label))
    line1 += word + " " * (max_length - len(word) + 1)
    line2 += full_label + " " * (max_length - len(full_label) + 1)

print(line1)
print(line2)
```

```python out
'EU    rejects German call to boycott British lamb .'
'B-ORG O       B-MISC O    O  O       B-MISC  O    O'
```

Și pentru un exemplu de amestecare a labelurilor `B-` și `I-`, iată ce ne oferă același cod pentru elementul din setul de antrenare de la indexul 4:

```python out
'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC   O  O              O  O   B-ORG    I-ORG O  O          O         B-PER  I-PER     O    O  O         O         O      O   O         O    O         O     O    B-LOC   O     O   O          O      O   O       O'
```

După cum putem vedea, entităților care cuprind două cuvinte, precum "Uniunea Europeană" și "Werner Zwingmann", li se atribuie un label `B-` pentru primul cuvânt și un label `I-` pentru al doilea.

> [!TIP]
> ✏️ **E rândul tău!** Afișați aceleași două propoziții cu labelurile POS sau chunking.

### Procesarea datelor[[processing-the-data]]

<Youtube id="iY2AZYdZAr0"/>

Ca de obicei, textele noastre trebuie să fie convertite în ID-uri token înainte ca modelul să le poată înțelege. După cum am văzut în [Capitolul 6](/course/chapter6/), o mare diferență în cazul sarcinilor de clasificare a tokenilor este că avem inputuri pre-tokenizate. Din fericire, API-ul tokenizerului poate face față acestei situații destul de ușor; trebuie doar să avertizăm `tokenizerul` cu un indicator special.

Pentru început, hai să creăm obiectul `tokenizer`. După cum am mai spus, vom utiliza un model BERT preantrenat, deci vom începe prin a descărca și a stoca în cache tokenizerul asociat:

```python
from transformers import AutoTokenizer

model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
```

Puteți înlocui `model_checkpoint` cu orice alt model preferat din [Hub](https://huggingface.co/models) sau cu un folder local în care ați salvat un model preantreant și un tokenizer. Singura constrângere este că tokenizerul trebuie să fie susținut de biblioteca 🤗 Tokenizers, pentru ca să existe o versiune "rapidă" disponibilă. Puteți vedea toate arhitecturile care vin cu o versiune rapidă în [acest tabel mare] (https://huggingface.co/transformers/#supported-frameworks), iar pentru a verifica dacă obiectul `tokenizer` pe care îl utilizați este într-adevăr susținut de 🤗 Tokenizers, vă puteți uita la atributul său `is_fast`:

```py
tokenizer.is_fast
```

```python out
True
```

Pentru a tokeniza un input pre-tokenizat, putem utiliza `tokenizer` ca de obicei și să adăugăm `is_split_into_words=True`:

```py
inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
```

```python out
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']
```

După cum putem vedea, tokenizerul a adăugat tokenii speciali utilizați de model (`[CLS]` la început și `[SEP]` la sfârșit) și a lăsat majoritatea cuvintelor neatinse. Cu toate acestea, cuvântul `lamb` a fost tokenizat în două subcuvinte, `la` și `##mb`. Acest lucru introduce o neconcordanță între inputurile noastre și labeluri: lista de labeluri are doar 9 elemente, în timp ce inputurile noastre au acum 12 tokeni. Să luăm în considerare tokenii speciali este ușor (știm că sunt la început și la sfârșit), dar trebuie să ne asigurăm că aliniem toate labelurile cu cuvintele corespunzătoare.

Din fericire, pentru că folosim un tokenizer rapid, avem acces la superputerile 🤗 Tokenizers, ceea ce înseamnă că putem mapa cu ușurință fiecare token la cuvântul corespunzător (după cum se vede în [Capitolul 6](/course/chapter6/3)):

```py
inputs.word_ids()
```

```python out
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
```

Cu puțină muncă, putem apoi extinde lista de labeluri pentru a se potrivi cu tokenii. Prima regulă pe care o vom aplica este că tokenii special primesc un label `-100`. Acest lucru se datorează faptului că în mod implicit `-100` este un indice care este ignorat în funcția de pierdere pe care o vom utiliza (cross-entropy). Apoi, fiecare token primește același label ca și tokenul care a inițiat cuvântul în care se află, deoarece fac parte din aceeași entitate. Pentru tokenii din interiorul unui cuvânt, dar care nu se află la început, înlocuim `B-` cu `I-` (deoarece tokenul nu începe entitatea):


```python
def align_labels_with_tokens(labels, word_ids):
    new_labels = []
    current_word = None
    for word_id in word_ids:
        if word_id != current_word:
            # Start of a new word!
            current_word = word_id
            label = -100 if word_id is None else labels[word_id]
            new_labels.append(label)
        elif word_id is None:
            # Special token
            new_labels.append(-100)
        else:
            # Same word as previous token
            label = labels[word_id]
            # If the label is B-XXX we change it to I-XXX
            if label % 2 == 1:
                label += 1
            new_labels.append(label)

    return new_labels
```

Hai să îl încercăm cu prima noastră propoziție:

```py
labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
```

```python out
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
```

După cum putem vedea, funcția noastră a adăugat `-100` pentru cei doi tokeni speciali de la început și de la sfârșit, și un nou `0` pentru cuvântul nostru care a fost împărțit în doi tokeni.

> [!TIP]
> ✏️ ** Rândul tău!** Unii cercetători preferă să atribuie un singur label pe cuvânt și să atribuie `-100` celorlalți subtokeni dintr-un cuvânt dat. Aceasta are loc pentru a evita ca cuvintele lungi care se împart în mai mulți subtokeni să contribuie puternic la pierdere. Modificați funcția anterioară pentru a alinia labelurile cu ID-urile de input urmând această regulă.

Pentru a preprocesa întregul nostru dataset, trebuie să tokenizăm toate inputurile și să aplicăm `align_labels_with_tokens()` pe toate labelurile. Pentru a profita de viteza tokenizerului nostru rapid, este mai bine să tokenizăm multe texte în același timp, așa că vom scrie o funcție care procesează o listă de exemple și vom folosi metoda `Dataset.map()` cu opțiunea `batched=True`. Singurul lucru diferit față de exemplul nostru anterior este că funcția `word_ids()` trebuie să obțină indexul exemplului din care dorim ID-urile cuvintelor atunci când inputurile către tokenizer sunt liste de texte (sau, în cazul nostru, liste de liste de cuvinte), așa că adăugăm și acest lucru:

```py
def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(
        examples["tokens"], truncation=True, is_split_into_words=True
    )
    all_labels = examples["ner_tags"]
    new_labels = []
    for i, labels in enumerate(all_labels):
        word_ids = tokenized_inputs.word_ids(i)
        new_labels.append(align_labels_with_tokens(labels, word_ids))

    tokenized_inputs["labels"] = new_labels
    return tokenized_inputs
```

Rețineți că nu am completat încă inputurile; vom face acest lucru mai târziu, atunci când vom crea batchurile cu un data collator.

Acum putem aplica toate aceste preprocesări dintr-o dată celorlalte fracțiuni ale datasetului nostru:

```py
tokenized_datasets = raw_datasets.map(
    tokenize_and_align_labels,
    batched=True,
    remove_columns=raw_datasets["train"].column_names,
)
```

Am făcut partea cea mai grea! Acum, că datele au fost preprocesate, antrenarea efectivă va semăna mult cu ceea ce am făcut în [Capitolul 3](/course/chapter3).

{#if fw === 'pt'}

## Fine-tuningul modelului cu `Trainer` API[[fine-tuning-the-model-with-the-trainer-api]]

Codul real care utilizează `Trainer` va fi același ca înainte; singurele modificări sunt modul în care datele sunt collated într-un batch și metric computation function.

{:else}

## Fine-tuningul modelului cu Keras[[fine-tuning-the-model-with-keras]]

Codul real care utilizează `Keras` va fi același ca înainte; singurele modificări sunt modul în care datele sunt collated într-un batch și metric computation function.

{/if}


### Data collation[[data-collation]]

Nu putem folosi doar un `DataCollatorWithPadding` ca în [Capitolul 3](/course/chapter3) deoarece acesta doar adaugă padding inputurilor(input IDs, attention mask, and token type IDs). Aici labelurile noastre ar trebui să fie padded exact în același mod ca și inputurile, astfel încât să rămână de aceeași dimensiune, folosind `-100` ca valoare, astfel încât predicțiile corespunzătoare să fie ignorate în calculul pierderilor.

Toate acestea sunt realizate de un [`DataCollatorForTokenClassification`](https://huggingface.co/transformers/main_classes/data_collator.html#datacollatorfortokenclassification). La fel ca `DataCollatorWithPadding`, acesta ia `tokenizer`-ul folosit pentru preprocesarea inputurilor:

{#if fw === 'pt'}

```py
from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
```

{:else}

```py
from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(
    tokenizer=tokenizer, return_tensors="tf"
)
```

{/if}

Pentru a testa acest lucru pe câteva sampleuri, îl putem apela doar pe o listă de exemple din setul nostru de antrenat tokenizat:

```py
batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
```

```python out
tensor([[-100,    3,    0,    7,    0,    0,    0,    7,    0,    0,    0, -100],
        [-100,    1,    2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])
```

Hai să comparăm acest lucru cu labelurile pentru primul și al doilea element din datasetul nostru:

```py
for i in range(2):
    print(tokenized_datasets["train"][i]["labels"])
```

```python out
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]
```

{#if fw === 'pt'}

După cum se poate observa, al doilea set de labeluri a fost padded la lungimea primului folosind `-100`.

{:else}

Data collatorul nostru de date este gata de utilizare! Acum să îl folosim pentru a crea un `tf.data.Dataset` cu metoda `to_tf_dataset()`. De asemenea, puteți utiliza `model.prepare_tf_dataset()` pentru a face acest lucru cu un pic mai puțin cod repetitiv - veți vedea acest lucru în unele dintre celelalte secțiuni ale acestui capitol.

```py
tf_train_dataset = tokenized_datasets["train"].to_tf_dataset(
    columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
    collate_fn=data_collator,
    shuffle=True,
    batch_size=16,
)

tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset(
    columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
    collate_fn=data_collator,
    shuffle=False,
    batch_size=16,
)
```


Următoarea oprire: modelul în sine.

{/if}

{#if fw === 'tf'}

### Definirea modelului[[defining-the-model]]

Deoarece lucrăm la o problemă de clasificare a tokenilor, vom utiliza clasa `TFAutoModelForTokenClassification`. Principalul lucru de care trebuie să ne amintim atunci când definim acest model este să transmitem informații privind numărul de labeluri pe care le avem. Cel mai simplu mod de a face acest lucru este să transmiteți acest număr cu argumentul `num_labels`, dar dacă dorim un inference widget frumos care să funcționeze ca cel pe care l-am văzut la începutul acestei secțiuni, este mai bine să setați în schimb corespondențele corecte ale labelurilor.

Acestea ar trebui să fie stabilite de două dicționare, `id2label` și `label2id`, care conțin corespondența de la ID la label și viceversa:

```py
id2label = {i: label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
``` 

Acum putem să le transmitem metodei `TFAutoModelForTokenClassification.from_pretrained()`, iar acestea vor fi setate în configurația modelului, apoi salvate corespunzător și încărcate în Hub:

```py
from transformers import TFAutoModelForTokenClassification

model = TFAutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

La fel ca atunci când am definit `TFAutoModelForSequenceClassification` în [Capitolul 3](/course/chapter3), crearea modelului emite un avertisment că unele weights nu au fost utilizate (cele din headul de preantrenare) și că alte weights sunt inițializate aleatoriu (cele din headul de clasificare a tokenilor noi) și că acest model ar trebui să fie antrenat. Vom face acest lucru într-un minut, dar mai întâi să verificăm de două ori că modelul nostru are numărul corect de labeluri:

```python
model.config.num_labels
```

```python out
9
```

> [!WARNING]
> ⚠️ Dacă aveți un model cu un număr greșit de labeluri, veți primi o eroare obscură atunci când apelați `model.fit()` mai târziu. Acest lucru poate fi enervant pentru debbuging, așa că asigurați-vă că faceți această verificare pentru a confirma că aveți numărul așteptat de labeluri.

### Fine-tuningul modelului[[fine-tuning-the-model]]

Acum suntem gata să ne antrenăm modelul! Totuși, mai avem doar câteva lucruri de făcut mai întâi: ar trebui să ne conectăm la Hugging Face și să definim hiperparametrii noștri de antrenare. Dacă lucrați într-un notebook, există o funcție convenabilă pentru a vă ajuta cu acest lucru:

```python
from huggingface_hub import notebook_login

notebook_login()
```

Aceasta va afișa un widget în care puteți introduce datele tale de autentificare Hugging Face.

Dacă nu lucrați într-un notebook, tastați următoarea linie în terminal:

```bash
huggingface-cli login
```

După autentificare, putem pregăti tot ce avem nevoie pentru a compila modelul nostru. 🤗 Transformers oferă o funcție convenabilă `create_optimizer()` care vă va oferi un optimizator `AdamW` cu setări adecvate pentru weight decay și learning rate decay, ambele îmbunătățind performanța modelului vsotru în comparație cu optimizatorul `Adam` încorporat:

```python
from transformers import create_optimizer
import tensorflow as tf

# Antrenarea în mixed-precision float16
# Comentați această linie dacă utilizați un GPU care nu va beneficia de acest lucru
tf.keras.mixed_precision.set_global_policy("mixed_float16")

# Numărul de etape de antrenare este numărul de sampleuri din dataset, împărțit la dimensiunea batch-ului, apoi multiplicat
# cu numărul total de epoci. Rețineți că tf_train_dataset de aici este un batched tf.data.Dataset,
# nu este originalul Hugging Face Dataset, deci len() este deja num_samples // batch_size.
num_epochs = 3
num_train_steps = len(tf_train_dataset) * num_epochs

optimizer, schedule = create_optimizer(
    init_lr=2e-5,
    num_warmup_steps=0,
    num_train_steps=num_train_steps,
    weight_decay_rate=0.01,
)
model.compile(optimizer=optimizer)
```

Rețineți, de asemenea, că nu furnizăm un argument `loss` la `compile()`. Acest lucru se datorează faptului că modelele pot calcula de fapt pierderea intern - dacă compilați fără o pierdere și furnizați labelurile în dicționarul de intrare (așa cum facem în dataseturile noastre), atunci modelul se va antrena folosind acea pierdere internă, care va fi adecvată pentru sarcina și tipul de model pe care le-ați ales.

În continuare, definim un `PushToHubCallback` pentru a încărca modelul nostru în Hub în timpul antrenării și pentru a potrivi modelul cu acel callback:

```python
from transformers.keras_callbacks import PushToHubCallback

callback = PushToHubCallback(output_dir="bert-finetuned-ner", tokenizer=tokenizer)

model.fit(
    tf_train_dataset,
    validation_data=tf_eval_dataset,
    callbacks=[callback],
    epochs=num_epochs,
)
```

Puteți specifica numele complet al repositoriului către care doriți să efectuați push cu argumentul `hub_model_id` (în special, va trebui să utilizați acest argument pentru a efectua push către o organizație). De exemplu, atunci când am trimis modelul către organizația [`huggingface-course`](https://huggingface.co/huggingface-course), am adăugat `hub_model_id="huggingface-course/bert-finetuned-ner"`. În mod implicit, repositoriul utilizat va fi în namespace-ul denumit după directory output pe care l-ați stabilit, de exemplu `"cool_huggingface_user/bert-finetuned-ner"`.

> [!TIP]
> 💡 Dacă directory output pe care îl utilizați există deja, acesta trebuie să fie o clonă locală a repositoriului către care doriți să faceți push. Dacă nu este, veți primi o eroare atunci când apelați `model.fit()` și va trebui să setați un nume nou.

Rețineți că, în timpul antrenamentului, de fiecare dată când modelul este salvat (aici, la fiecare epocă), acesta este încărcat pe Hub în fundal. În acest fel, veți putea să reluați formarea pe o altă mașină, dacă este necesar.

În acest stadiu, puteți utiliza inference widget de pe Model Hub pentru a testa modelul vostru și pentru a-l partaja cu prietenii. Ați făcut fine-tune cu succes unui model pentru o sarcină de clasificare a tokenilor - felicitări! Dar cât de bun este modelul nostru, de fapt? Ar trebui să evaluăm anumiți parametri pentru a afla.

{/if}


### Metrici[[metrics]]

{#if fw === 'pt'}

Pentru ca `Trainer` să calculeze o metrică în fiecare epocă, va trebui să definim o funcție `compute_metrics()` care primește matricele de predicții și labelurile și returnează un dicționar cu numele și valorile metricilor.

Frameworkul tradițional utilizat pentru a evalua predicția clasificării a tokenilor este [*seqeval*](https://github.com/chakki-works/seqeval). Pentru a utiliza această metrică, trebuie mai întâi să instalăm biblioteca *seqeval*:

```py
!pip install seqeval
```

Apoi îl putem încărca prin intermediul funcției `evaluate.load()` așa cum am făcut în [Capitolul 3](/course/chapter3):

{:else}

Frameworkul tradițional utilizat pentru a evalua predicția clasificării tokenilor este [*seqeval*](https://github.com/chakki-works/seqeval). Pentru a utiliza această metrică, trebuie mai întâi să instalăm biblioteca *seqeval*:

```py
!pip install seqeval
```

Apoi îl putem încărca prin intermediul funcției `evaluate.load()` așa cum am făcut în [Capitolul 3](/course/chapter3):

{/if}

```py
import evaluate

metric = evaluate.load("seqeval")
```

Această metrică nu se comportă ca precizia standard: de fapt, va lua listele de labeluri ca șiruri de caractere, nu ca numere întregi, deci va trebui să decodificăm complet predicțiile și labelurile înainte de a le trece în metrice. Să vedem cum funcționează. În primul rând, vom obține labelurile pentru primul nostru exemplu de antrenare:

```py
labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
```

```python out
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']
```

Putem apoi crea predicții false pentru acestea prin simpla schimbare a valorii la indexul 2:

```py
predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])
```

Rețineți că metrica ia o listă de predicții (nu doar una) și o listă de labels. Iată rezultatul:

```python out
{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
 'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
 'overall_precision': 1.0,
 'overall_recall': 0.67,
 'overall_f1': 0.8,
 'overall_accuracy': 0.89}
```

{#if fw === 'pt'}

Acesta trimite înapoi o mulțime de informații! Noi obținem precizia, recallul și scorul F1 pentru fiecare entitate în parte, precum și scorul general. Pentru calculul nostru metric, vom păstra doar scorul global, dar nu ezitați să modificați funcția `compute_metrics()` pentru a returna toate metricile pe care doriți să le raportați.

Această funcție `compute_metrics()` ia mai întâi argmaxul logiturilor pentru a le converti în predicții (ca de obicei, logiturile și probabilitățile sunt în aceeași ordine, deci nu trebuie să aplicăm softmaxul). Apoi trebuie să convertim atât labelurile, cât și predicțiile din numere întregi în șiruri de caractere. Eliminăm toate valorile în care labelul este `-100`, apoi transmitem rezultatele metodei `metric.compute()`:

```py
import numpy as np


def compute_metrics(eval_preds):
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": all_metrics["overall_precision"],
        "recall": all_metrics["overall_recall"],
        "f1": all_metrics["overall_f1"],
        "accuracy": all_metrics["overall_accuracy"],
    }
```

Acum că acest lucru este făcut, suntem aproape gata să definim `Trainer`-ul nostru. Avem nevoie doar de un `model` pentru a face fine-tune!

{:else}

Aceasta trimite înapoi o mulțime de informații! Obținem precizia, recallul și scorul F1 pentru fiecare entitate în parte, precum și în ansamblu. Acum să vedem ce se întâmplă dacă încercăm să folosim predicțiile modelului nostru real pentru a calcula niște scoruri reale.

TensorFlow nu apreciază concatenarea predicțiilor noastre, deoarece acestea au lungimi de secvențe variabile. Aceasta înseamnă că nu putem folosi pur și simplu `model.predict()` - dar asta nu ne va opri. Vom obține unele predicții pe rând și le vom concatena într-o singură listă mare și lungă, eliminând simbolurile `-100` care indică mascarea/paddingul, apoi vom calcula metrici pe lista de la sfârșit:

```py
import numpy as np

all_predictions = []
all_labels = []
for batch in tf_eval_dataset:
    logits = model.predict_on_batch(batch)["logits"]
    labels = batch["labels"]
    predictions = np.argmax(logits, axis=-1)
    for prediction, label in zip(predictions, labels):
        for predicted_idx, label_idx in zip(prediction, label):
            if label_idx == -100:
                continue
            all_predictions.append(label_names[predicted_idx])
            all_labels.append(label_names[label_idx])
metric.compute(predictions=[all_predictions], references=[all_labels])
```


```python out
{'LOC': {'precision': 0.91, 'recall': 0.92, 'f1': 0.91, 'number': 1668},
 'MISC': {'precision': 0.70, 'recall': 0.79, 'f1': 0.74, 'number': 702},
 'ORG': {'precision': 0.85, 'recall': 0.90, 'f1': 0.88, 'number': 1661},
 'PER': {'precision': 0.95, 'recall': 0.95, 'f1': 0.95, 'number': 1617},
 'overall_precision': 0.87,
 'overall_recall': 0.91,
 'overall_f1': 0.89,
 'overall_accuracy': 0.97}
```

Cum s-a descurcat modelul tău, comparativ cu al nostru? Dacă ați obținut cifre similare, antrenarea a fost un succes!

{/if}

{#if fw === 'pt'}

### Definirea modelului[[defining-the-model]]

Deoarece lucrăm la o problemă de clasificare a tokenilor, vom utiliza clasa `AutoModelForTokenClassification`. Principalul lucru de care trebuie să ne amintim atunci când definim acest model este să transmitem informații privind numărul de labeluri pe care le avem. Cel mai simplu mod de a face acest lucru este să transmiteți acest număr cu argumentul `num_labels`, dar dacă dorim un inferencea widget frumos care să funcționeze ca cel pe care l-am văzut la începutul acestei secțiuni, este mai bine să setați în schimb corespondențele corecte ale labelurilor.

Acestea ar trebui să fie stabilite de două dicționare, `id2label` și `label2id`, care conțin corespondențele de la ID la labeluri și viceversa:

```py
id2label = {i: label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
```

Acum putem doar să le transmitem metodei `AutoModelForTokenClassification.from_pretrained()`, iar acestea vor fi setate în configurația modelului și apoi salvate și încărcate corespunzător în Hub:

```py
from transformers import AutoModelForTokenClassification

model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

La fel ca atunci când am definit `AutoModelForSequenceClassification` în [Capitolul 3](/course/chapter3), crearea modelului emite un avertisment că unele weights nu au fost utilizate (cele din headul de antrenare) și alte weights sunt inițializate aleatoriu (cele din headul de clasificare a tokenilor noi) și că acest model ar trebui să fie format. Vom face acest lucru într-un minut, dar mai întâi să verificăm de două ori că modelul nostru are numărul corect de labeluri:

```python
model.config.num_labels
```

```python out
9
```

> [!WARNING]
> ⚠️ Dacă aveți un model cu un număr greșit de labeluri, veți primi o eroare obscură atunci când apelați metoda `Trainer.train()` mai târziu (ceva de genul "CUDA error: device-side assert triggered"). Aceasta este cauza numărul unu a erorilor raportate de utilizatori pentru astfel de erori, așa că asigurați-vă că faceți această verificare pentru a confirma că aveți numărul de labeluri așteptat.

### Fine-tuningul modelului[[fine-tuning-the-model]]

Acum suntem gata să ne antrenăm modelul! Trebuie doar să facem ultimele două lucruri înainte de a defini modelul nostru `Trainer`: să ne conectăm la Hugging Face și să definim argumentele de antrenare. Dacă lucrați într-un notebook, există o funcție convenabilă pentru a vă ajuta cu acest lucru:

```python
from huggingface_hub import notebook_login

notebook_login()
```

Aceasta va afișa un widget în care puteți introduce datele tale de autentificare Hugging Face.

Dacă nu lucrați într-un notebook, tastați următoarea linie în terminal:

```bash
huggingface-cli login
```

Odată făcut acest lucru, putem defini`TrainingArguments`:

```python
from transformers import TrainingArguments

args = TrainingArguments(
    "bert-finetuned-ner",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)
```

Ați mai văzut cele mai multe dintre acestea: stabilim niște hiperparametri (cum ar fi learning rate, numărul de epoci pentru care să ne antrenăm și weights decay) și specificăm `push_to_hub=True` pentru a indica faptul că dorim să salvăm modelul și să îl evaluăm la sfârșitul fiecărei epoci și că dorim să încărcăm rezultatele noastre în Model Hub. Rețineți că puteți specifica numele repositoriului către care doriți să faceți push cu argumentul `hub_model_id` (în special, va trebui să utilizați acest argument pentru a face push către o organizație). De exemplu, atunci când am trimis modelul către organizația [`huggingface-course`](https://huggingface.co/huggingface-course), am adăugat `hub_model_id="huggingface-course/bert-finetuned-ner"` la `TrainingArguments`. În mod implicit, repositoriul utilizat va fi în namespaceul tău și denumit după output directory-ul pe care l-ați setat, deci în cazul nostru va fi `"sgugger/bert-finetuned-ner"`.

> [!TIP]
> 💡 Dacă output directory-ul pe care îl utilizați există deja, acesta trebuie să fie o clonă locală a repositoriul către care doriți să faceți push. Dacă nu este așa, veți primi o eroare la definirea `Trainer` și va trebui să setați un nume nou.

În final, transmitem totul către `Trainer` și lansăm antrenarea:

```python
from transformers import Trainer

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
)
trainer.train()
```

Rețineți că, în timpul antrenării, de fiecare dată când modelul este salvat (aici, la fiecare epocă), acesta este încărcat pe Hub în fundal. În acest fel, veți putea să reluați antrenareape o altă mașină, dacă este necesar.

Odată ce antrenamentul este complet, folosim metoda `push_to_hub()` pentru a ne asigura că încărcăm cea mai recentă versiune a modelului:

```py
trainer.push_to_hub(commit_message="Training complete")
```

Această comandă returnează URL-ul comitului pe care tocmai l-ai făcut, dacă doriți să o inspectați:

```python out
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
```

De asemenea, `Trainer` redactează un model card cu toate rezultatele evaluării și o încarcă. În acest stadiu, puteți utiliza inference widgetul de pe Model Hub pentru a testa modelul și a-l partaja cu prietenii. Ați făcut fine-tune cu succes un model pentru o sarcină de clasificare a tokenilor - felicitări!

Dacă doriți să vă scufundați puțin mai profund în bucla de antrenare, vă vom arăta acum cum să faceți același lucru utilizând 🤗 Accelerate.

## Un training loop personalizat[[a-custom-training-loop]]

Să aruncăm acum o privire la training loopul complet, astfel încât să puteți personaliza cu ușurință părțile de care aveți nevoie. Va semăna foarte mult cu ceea ce am făcut în [Capitolul 3](/course/chapter3/4), cu câteva modificări pentru evaluare.

### Pregătiți totul pentru antrenare[[preparing-everything-for-training]]

Mai întâi trebuie să construim `DataLoader`s din dataseturile noastre. Vom reutiliza `data_collator` ca un `collate_fn` și vom amesteca setul de antrenare, dar nu și setul de validare:

```py
from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=8,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)
```

În continuare, reinstanțiem modelul, pentru a ne asigura că nu continuăm fine-tuningul de dinainte, ci pornim din nou de la modelul preantrenat BERT:

```py
model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

Atunci vom avea nevoie de un optimizator. Vom folosi clasicul `AdamW`, care este ca `Adam`, dar cu o corecție în modul în care se aplică weight decay-ul:

```py
from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=2e-5)
```

Odată ce avem toate aceste obiecte, le putem trimite la metoda `accelerator.prepare()`:

```py
from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)
```

> [!TIP]
> 🚨 Dacă vă antrenați pe un TPU, va trebui să mutați tot codul începând de la celula de mai sus într-o funcție de antrenament dedicată. Consultați [Capitolul 3](/course/chapter3) pentru mai multe detalii.

Acum că am trimis `train_dataloader` la `accelerator.prepare()`, putem utiliza lungimea acestuia pentru a calcula numărul de pași de antrenare. Rețineți că ar trebui să facem întotdeauna acest lucru după ce pregătim dataloaderul, deoarece această metodă îi va modifica lungimea. Utilizăm un classic liner schedule de la rata de învățare la 0:

```py
from transformers import get_scheduler

num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)
```

În cele din urmă, pentru a trimite modelul nostru către Hub, va trebui să creăm un obiect `Repository` într-un folder de lucru. În primul rând, conectați-vă la Hugging Face, dacă nu sunteți deja conectat. Vom determina numele repositoriului pornind de la ID-ul modelului pe care dorim să îl dăm modelului nostru (nu ezitați să înlocuiți `repo_name` cu propriul nume; trebuie doar să conțină numele vostru de utilizator, ceea ce face funcția `get_full_repo_name()`):

```py
from huggingface_hub import Repository, get_full_repo_name

model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
```

```python out
'sgugger/bert-finetuned-ner-accelerate'
```

Apoi putem clona acel repositoriu într-un folder local. Dacă există deja, acest folder local ar trebui să fie o clonă existentă a repositoriului cu care lucrăm:

```py
output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
```

Acum putem încărca orice salvăm în `output_dir` prin apelarea metodei `repo.push_to_hub()`. Acest lucru ne va ajuta să încărcăm modelele intermediare la sfârșitul fiecărei epoci.

### Loopul de antrenare[[training-loop]]

Acum suntem pregătiți să scriem bucla de antrenare completă. Pentru a simplifica partea sa de evaluare, definim funcția `postprocess()` care preia predicțiile și labelurile și le convertește în liste de șiruri de caractere, așa cum se așteaptă obiectul nostru `metric`:

```py
def postprocess(predictions, labels):
    predictions = predictions.detach().cpu().clone().numpy()
    labels = labels.detach().cpu().clone().numpy()

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    return true_labels, true_predictions
```

Apoi putem scrie bucla de antrenare. După definirea unei bare de progres pentru a urmări modul în care decurge antrenarea, bucla are trei părți:

- Antrenarea în sine, care este iterația clasică peste `train_dataloader`, trecerea înainte prin model, apoi trecerea înapoi și pasul optimizatorului.
- Evaluarea, în care există o noutate după obținerea outputurilor modelului nostru pe un batch: din moment ce două procese pot ar fi putut face padding inputurilor și labelurile la forme diferite, trebuie să folosim `accelerator.pad_across_processes()` pentru a face predicțiile și labelurile să aibă aceeași formă înainte de a apela metoda `gather()`. Dacă nu facem acest lucru, evaluarea va da eroare sau se va bloca pentru totdeauna. Apoi trimitem rezultatele la `metric.add_batch()` și apelăm `metric.compute()` odată ce bucla de evaluare s-a încheiat.
- Salvarea și încărcarea, unde mai întâi salvăm modelul și tokenizerul, apoi apelăm `repo.push_to_hub()`. Observați că folosim argumentul `blocking=False` pentru a spune bibliotecii 🤗 Hub să efectueze push-ul într-un proces asincron. În acest fel, antrenamentul continuă normal, iar această instrucțiune (lungă) este executată în fundal.

Iată codul complet pentru bucla de antrenare:

```py
from tqdm.auto import tqdm
import torch

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # Training
    model.train()
    for batch in train_dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # Evaluation
    model.eval()
    for batch in eval_dataloader:
        with torch.no_grad():
            outputs = model(**batch)

        predictions = outputs.logits.argmax(dim=-1)
        labels = batch["labels"]

        # Necessary to pad predictions and labels for being gathered
        predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
        labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)

        predictions_gathered = accelerator.gather(predictions)
        labels_gathered = accelerator.gather(labels)

        true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
        metric.add_batch(predictions=true_predictions, references=true_labels)

    results = metric.compute()
    print(
        f"epoch {epoch}:",
        {
            key: results[f"overall_{key}"]
            for key in ["precision", "recall", "f1", "accuracy"]
        },
    )

    # Save and upload
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )
```

În cazul în care este prima dată când vedeți un model salvat cu 🤗 Accelerate, să ne oprim puțin pentru a inspecta cele trei linii de cod care îl însoțesc:

```py
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
```

Prima linie se explică de la sine: aceasta spune tuturor proceselor să aștepte până când toată lumea se află în etapa respectivă înainte de a continua. Acest lucru are rolul de a ne asigura că avem același model în fiecare proces înainte de a salva. Apoi luăm `unwrapped_model`, care este modelul de bază pe care l-am definit. Metoda `accelerator.prepare()` modifică modelul pentru a funcționa în antrenarea distribuită, deci nu va mai avea metoda `save_pretrained()`; metoda `accelerator.unwrap_model()` anulează acest pas. În cele din urmă, apelăm metoda `save_pretrained()`, dar îi spunem să folosească metoda `accelerator.save()` în loc de `torch.save()`.

Odată făcut acest lucru, ar trebui să aveți un model care produce rezultate destul de asemănătoare cu cel antrenat cu `Trainer`. Puteți verifica modelul pe care l-am antrenat folosind acest cod la [*huggingface-course/bert-finetuned-ner-accelerate*] (https://huggingface.co/huggingface-course/bert-finetuned-ner-accelerate). Și dacă doriți să testați orice modificări ale buclei de antrenare, le puteți implementa direct prin editarea codului prezentat mai sus!


{/if}

## Utilizarea model fine-tuned[[using-the-fine-tuned-model]]

V-am arătat deja cum puteți utiliza modelul pe care l-am ajustat pe Model Hub cu inference widget. Pentru a-l utiliza la nivel local într-un `pipeline`, trebuie doar să specificați identificatorul de model corespunzător:

```py
from transformers import pipeline

# Replace this with your own checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
    "token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
```

```python out
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

Grozav! Modelul nostru funcționează la fel de bine ca cel implicit pentru aceast pipeline!